crackmeUK 解析手引き その3(NORMAL)


それでは、crackme_UK 、NORMAL の解説に入りたいと思います。

OllyDBGを起動して、CrackMe UK を読み込んで下さい。
そのまま F9 キーで実行しましょう。CrackMe UK のウィンドウが表示されたかと思います。

EASY の正解パスの形式に倣い "CR0123-4567-89AB" と入力して、 「検索→現在のモジュール名」から
GetWindowTextA ( Address 0041C430  USER32.GetWindowTextA) にブレークポイントを仕掛けてから OK ボタンを押すと
00417135 でブレークします。


0041712A  |. FF7424 08      push    dword ptr ss:[esp+8]                        ; /Count
0041712E  |. FF7424 08      push    dword ptr ss:[esp+8]                        ; |Buffer
00417132  |. FF71 1C        push    dword ptr ds:[ecx+1C]                       ; |hWnd
00417135  |. FF15 30C44100  call    near dword ptr ds:[<&USER32.GetWindowTextA> ; \GetWindowTextA


F8 キーを3回押して関数を抜けましょう。

00401EA0  |. BF D0594200    mov     edi, CRACKME.004259D0            ;  ASCII "9876543210"

ここに出るはずです。そのすぐ下にこんな命令があります。


00401EA5  |. 83C9 FF        or      ecx, FFFFFFFF            ; ecx 初期化
00401EA8  |. 33C0           xor     eax, eax                 ; eax 初期化
00401EAA  |. F2:AE          repne   scas byte ptr es:[edi]
00401EAC  |. F7D1           not     ecx
00401EAE  |. 49             dec     ecx
00401EAF  |. 83F9 10        cmp     ecx, 10
00401EB2  |. 0F85 DD000000  jnz     CRACKME.00401F95


edi レジスタには入力文字列のポインタが入っていますが、 repne はストリングス命令と呼ばれるものの一つで、
 eax レジスタとの比較結果が等しくないか ecx ≠ 0 であれば同じ命令を繰り返し実行します。
今回は xor  eax, eax 命令でゼロ初期化、repne 命令ではバイト型でキャストされているので al (= 0) と入力文字を
先頭から順に比較、すなわちnull 文字が見つかるまで繰り返されます。
その際 edi レジスタの値は自動的にインクリメントされます。

さらに ecx レジスタ命令もその都度デクリメントされますが、ecx レジスタの値に注目です。
repne 命令を抜けると、ecx=FFFFFFEE となりましたが、これは -18 を意味しています。

正直この部分はさして重要というわけでもないのですが、ストリングス命令があるということで
解説いってみましょう。ただ、その前に2の補数について簡単に説明したいとおもいます。

多くのコンピュータでは負数を「2の補数」というもので表します。
ご存じの通り、符号ビットは一番左にあるわけですが、
例えば 7 を2進数(8ビット)表記で表すと、 00000111 、-7 は 11111001 です。
00000111(7) はわかると思いますが、符号ビットは一番左にあるのに
なぜ -7 は 10000111 ではなく 11111001 なのでしょうか。
これが2の補数の特徴なのですが、その出し方は

1) まず +7 の2進数を求めます(8ビット表記)。   .. 00000111
2) ビットを反転(not)する。これは1の補数である。  .. 11111000
3) 1の補数に1を加えると2の補数になる。         .. 11111001

しかし、なぜこんな一見面倒なことをやるのでしょうか。それは、
負数を2の補数で表すと、減算を加算で表現できるからです。

   00001010  <-  10
+) 11111001  <-  -7
-----------
  100000011

溢れたビットを潰すと

   00000011  <-   3

となるわけです。
さて、実際のコードですが、


00401EA0  |. BF D0594200    mov     edi, CRACKME.004259D0    ;  ASCII "9876543210"
00401EA5  |. 83C9 FF        or      ecx, FFFFFFFF            ; ecx 初期化(FFFFFFFFh = -1)
00401EA8  |. 33C0           xor     eax, eax                 ; eax 初期化
00401EAA  |. F2:AE          repne   scas byte ptr es:[edi]
00401EAC  |. F7D1           not     ecx
00401EAE  |. 49             dec     ecx
00401EAF  |. 83F9 10        cmp     ecx, 10                  ; ecx には文字数が入る
00401EB2  |. 0F85 DD000000  jnz     CRACKME.00401F95


の


00401EAC  |. F7D1           not     ecx
00401EAE  |. 49             dec     ecx    ; inc  eax ではない!


この部分が該当するのですが、 inc  ecx ではなくて dec  ecx となっています。
実はこれには理由があります。
まずアドレス 00401EA0 にブレークポイントを仕掛けましょう。
その後、パスワードを入れずに OK ボタンを押してみてください。ブレークしましたね。

00401EA5  |. 83C9 FF        or      ecx, FFFFFFFF            ; ecx 初期化(FFFFFFFFh = -1)

まず、ecx レジスタに -1 が入ります。なぜ 0 では無いのか?
ストリングス命令において ecx レジスタはカウンタの役割を果たしており、
ecx = 0 になるとループが終了してしまうからです。

00401EA8  |. 33C0           xor     eax, eax                 ; eax 初期化

これは文字列(というよりも連続したデータ)と比較したい値をセットしています。
ここでは 0 ですね。

00401EAA  |. F2:AE          repne   scas byte ptr es:[edi]

ストリングス命令です。edi レジスタには文字列のポインタが入っています。
ここでは繰り返し実行はせずにすぐ抜けるのですが、ecx は一度デクリメントされ
ecx = FFFFFFFE(-2) となります。


00401EAC  |. F7D1           not     ecx
00401EAE  |. 49             dec     ecx


そして ecx レジスタの値に対し、2の補数をとっているような気がしますが、

00401EAE  |. 49             dec     ecx

inc    eax ではありません。もし純粋に2の補数を取るのであれば、デクリメント命令を
実行する前の ecx レジスタの値は ecx = 0 でないといけないはずです。
しかし、デクリメント命令実行前の ecx レジスタの値は FFFFFFFE(-2) です。


or      ecx, FFFFFFFF
repne   scas byte ptr es:[edi]


この2つの命令で実際の文字数と比べて2少なくなっています。この値のズレを補正するために


~ecx + 1 - 2    =    ~ecx - 1
         ~~~              ~~~

としているわけです。従って dec    ecx になるというわけです。
お分かり頂けたでしょうか?

単に文字列を取得したいのであれば、lstrlen でもいけるのですが、
asm 的に処理したいのであれば、こういう方法があるのですね。

VC がこんなコードを出力するということに感心しました(笑)


さて、 ecx レジスタには入力文字列の文字数が入ったわけですが、cmp  ecx, 10 で、
入力パスが 16 文字であるかチェックしています。今回は入力パスが 16 文字なので問題ありません。
次に、


00401EB8  |. 6A 02          push    2
00401EBA  |. 68 58224200    push    CRACKME.00422258                 ;  ASCII "CR"
00401EBF  |. 68 D0594200    push    CRACKME.004259D0                 ;  ASCII "CR0123-4567-89AB"
00401EC4  |. E8 C72D0000    call    CRACKME.00404C90      ; 怪しい関数
00401EC9  |. 83C4 0C        add     esp, 0C
00401ECC  |. 85C0           test    eax, eax
00401ECE  |. 0F85 C1000000  jnz     CRACKME.00401F95


と、ありますが、引数の形で関数がやっていることはほぼわかりますね。
先頭2文字が "CR" であるかチェックしています。次にいきましょう。


00401ED4  |. 8A0D D6594200  mov     cl, byte ptr ds:[4259D6]  ; [004259D6] = 入力パス 7 文字目
00401EDA  |. B0 2D          mov     al, 2D                    ; 2Dh = '-'
00401EDC  |. 3AC8           cmp     cl, al
00401EDE  |. 0F85 B1000000  jnz     CRACKME.00401F95
00401EE4  |. 3805 DB594200  cmp     byte ptr ds:[4259DB], al  ; [004259DB] = 入力パス 12 文字目
00401EEA  |. 0F85 A5000000  jnz     CRACKME.00401F95


ここでは入力パスの 7 文字目、12 文字目がハイフンであるかどうかをチェックしています。
この点については、ダンプウィンドウにフォーカスを合わせ Ctrl + G でアドレス 004259D6 に
移動してみるとよくわかると思います。
これより、入力パスの形態は "CRxxxx-xxxx-xxxx" のような形であることがわかりました。
EASY とは違い、ハイフンの位置も決められているようです。
さて、次に行きます。


00401EF0  |. BE D2594200    mov     esi, CRACKME.004259D2            ;  ASCII "0123-4567-89AB"
00401EF5  |> 56             /push    esi                             ;  CRACKME.004259D2
00401EF6  |. E8 35FEFFFF    |call    CRACKME.00401D30
00401EFB  |. 83C4 04        |add     esp, 4
00401EFE  |. 85C0           |test    eax, eax
00401F00  |. 0F84 8F000000  |je      CRACKME.00401F95
00401F06  |. 83C6 05        |add     esi, 5
00401F09  |. 81FE E1594200  |cmp     esi, CRACKME.004259E1
00401F0F  |.^7C E4          \jl      short CRACKME.00401EF5
00401F11  |. 8B83 D0000000  mov     eax, dword ptr ds:[ebx+D0]


このループはなにをやっているのでしょうか。少々見当がつかないと思いますが、
ダンプウィンドウに表示されている入力パスの文字列を眺めつつトレースするとよくわかります。


004259D0  43 52 30 31 32 33 2D 34 35 36 37 2D 38 39 41 42  CR0123-4567-89AB  ; ループ前
004259D0  43 52 00 01 02 03 2D 04 05 06 07 2D 08 09 0A 0B  CR....-....-....  ; ループ後


このループは、文字としての値を16進数の値に変更しているようです。
関数の最後の方で "Congraturations!!!" という文字列が参照されていますが、
ここから先のキーチェックルーチンが見つからず苦労された方が多いと思います。
この先で呼び出されている API を確認してみましょう。


00401F47  |. FF15 64C14100  call    near dword ptr ds:[<&KERNEL32.ResumeThread>        ; \ResumeThread
00401F53  |. FF15 68C14100  call    near dword ptr ds:[<&KERNEL32.WaitForSingleObject> ; \WaitForSingleObject
00401F62  |. FF15 6CC14100  call    near dword ptr ds:[<&KERNEL32.GetExitCodeThread>   ; \GetExitCodeThread


これらの API からキーチェック自体は別スレッドにて行っているようです。


00401F17  |. 6A 00          push    0
00401F19  |. 6A 04          push    4
00401F1B  |. 6A 00          push    0
00401F1D  |. 8D4C24 1C      lea     ecx, dword ptr ss:[esp+1C]
00401F21  |. 6A 00          push    0
00401F23  |. 51             push    ecx
00401F24  |. 68 701D4000    push    CRACKME.00401D70                         ; ?
00401F29  |. C74424 28 D059>mov     dword ptr ss:[esp+28], CRACKME.004259D0  ;  ASCII "CR"
00401F31  |. 894424 2C      mov     dword ptr ss:[esp+2C], eax
00401F35  |. E8 7D0A0100    call    CRACKME.004129B7   <-- おそらくこの call で別スレッドを作成している


それでは今まで仕掛けたブレークポイントは解除して、別スレッドを生成する API, CreateThread 
( Address 0041C0E8 KERNEL32.CreateThread )に「検索→現在のモジュール名」からブレークポイントを
仕掛けて実行してみましょう。(すでに用意されているスレッドを動かす ResumeThread も重要な API ですが今回はパス)
ブレークしましたか?それでは CreateThread の引数を見てみましょう。
画面右下のスタックウィンドウをみて下さい。
(アドレスはOS環境により異なりますので、置き換えて読んで下さい。)

0065F7B0   00000000  |pSecurity = NULL
0065F7B4   00000000  |StackSize = 0
0065F7B8   00405262  |ThreadFunction = CRACKME.00405262  <---- 別スレッドとして起動するルーチンの先頭アドレス
0065F7BC   00990570  |pThreadParm = 00990570
0065F7C0   00000004  |CreationFlags = CREATE_SUSPENDED
0065F7C4   0099061C  \pThreadId = 0099061C


重要なのは ThreadFunction です。別スレッドとして起動するルーチンの先頭アドレスが格納されています。
Ctrl + G から 00405262 と入力して該当ルーチンに飛んでブレークポイントを仕掛けましょう。
F9 を押すとブレークしました。ちょっと長そうなルーチンなので、とりあえず F8 連打で適当に進めて
みると、アドレス 004052BE で停止してしまいます。

004052BE   . FF56 48        call    near dword ptr ds:[esi+48]             ;  CRACKME.00412874

どうやらスレッドの優先順位を変えているようです。右側に表示されているアドレス 00412874 に
F7 キーでジャンプしてみましょう。


00412874   . B8 D0 B8 41 00       ascii   "クミクA",0         ; もしブレークポイントを仕掛けるなら
00412879   . E8 2627FFFF          call    CRACKME.00404FA4 ; ここに仕掛けると警告は出ない


ここの命令も長いですが、適当に F8 連打すると、アドレス 0041293E で途中で止まってしまいました。


0041293B   . 6A FF                push    -1                                                   ; /Timeout = INFINITE
0041293D   . 53                   push    ebx                                                  ; |hObject
0041293E   . FF15 68C14100        call    near dword ptr ds:[<&KERNEL32.WaitForSingleObject>]  ; \WaitForSingleObject


この命令は最初の方でも出てきましたが、次のいずれかが成立すると制御を返す命令です。

・指定されたオブジェクトがシグナル状態になった。
・タイムアウト時間が経過した。

タイムアウト時間については、INFINITE が指定されているため、オブジェクトがシグナル状態になるまで待機し続けます。
 とりあえずこれまでのブレークポイントをすべて解除して、FindCloseChangeNotificationの後の命令アドレス、
0041294B にブレークポイントを仕掛けて再実行した方が良さそうです。
解析では基本的にトライ&エラーの繰り返しです。根気強くいきましょう。


0041294B   . 8B46 50              mov     eax, dword ptr ds:[esi+50]  ;  CRACKME.00401D70
0041294E   . 85C0                 test    eax, eax
00412950   . 74 08                je      short CRACKME.0041295A
00412952   . FF76 4C              push    dword ptr ds:[esi+4C]       ; [esi+4C] = 入力パスの先頭アドレス
00412955   . FFD0                 call    eax                       ; call  00401D70  と同じ


さて、五月蠅い API 群を抜けるとこのような比較分岐に当たりました。ここで eax レジスタには 00401D70 が
格納されていますが、この値は先ほども出てきました。


00401F24  |. 68 701D4000    push    CRACKME.00401D70                         ; ←ここ
00401F29  |. C74424 28 D059>mov     dword ptr ss:[esp+28], CRACKME.004259D0  ;  ASCII "CR"
00401F31  |. 894424 2C      mov     dword ptr ss:[esp+2C], eax
00401F35  |. E8 7D0A0100    call    CRACKME.004129B7   <-- おそらくこの call で別スレッドを作成している


アドレス 00401D70, 非常に怪しいですね。
次の call  eax でジャンプしていますが、直前の push 命令で入力パスの先頭アドレスをプッシュしています。
これは気づきにくいかもしれませんが、このあたりは経験に基づくカンのようなものに頼らなければなりません。
理論的に説明が出来なくて申し訳ありません。
とにかく、キーチェック部分を別スレッドで処理するだけでこんなに難しくなるのですね。


*OllyDbg は Win2000 環境で真価を発揮するわけですが、Win2000 環境なら
メモリブレークポイントが使えるので入力パスのアドレスに仕掛ければ直ぐさまパスチェッカ内部でブレークすると思うのですが、
Win2000 では未確認です。各自確認してみてください。ちなみにこの方法はcrackme #20 で有効な手段です。


さて、アドレス 00401D70 に行ったところでいよいよ入力パス比較部分に入りました。
このチェッカは非常にややこしい上、トレースしてもレジスタやメモリに正解パスが見えないので、
かなり難しいと思います。とにかく行ってみましょう。


00401D7A   . 8B79 04        mov     edi, dword ptr ds:[ecx+4]   ; edi = 固有 ID
00401D7D   . 8B01           mov     eax, dword ptr ds:[ecx]     ; パスの先頭アドレス


ここで固有 ID が出てきています。この値は頻繁に参照されるので要注意。

固有 ID 表示が 11-5599CC であれば、 edi = 115599CC となります。
次に、00401D87 - 00401D9A のループで、入力パスが "CR0123-4567-89AB" であれば、
esi = 0123 となるはずです。ここから先はとてもややこしい処理となっているため、
逆汗リストの右側にコメントを入れてます。参照して下さい。



00401D70   . 83EC 0C        sub     esp, 0C
00401D73   . 8B4C24 10      mov     ecx, dword ptr ss:[esp+10]
00401D77   . 53             push    ebx
00401D78   . 56             push    esi
00401D79   . 57             push    edi
00401D7A   . 8B79 04        mov     edi, dword ptr ds:[ecx+4]   ; edi = 固有 ID
00401D7D   . 8B01           mov     eax, dword ptr ds:[ecx]     ; パスの先頭アドレス
00401D7F   . 33F6           xor     esi, esi
00401D81   . 897C24 1C      mov     dword ptr ss:[esp+1C], edi
00401D85   . 33C9           xor     ecx, ecx
00401D87   > 8A5408 02      mov     dl, byte ptr ds:[eax+ecx+2] ; パスの3文字目から1文字ずつ
00401D8B   . 81E2 FF000000  and     edx, 0FF
00401D91   . C1E6 04        shl     esi, 4
00401D94   . 03F2           add     esi, edx
00401D96   . 41             inc     ecx
00401D97   . 83F9 04        cmp     ecx, 4                      ; "CR0123-4567-89AB" なら
00401D9A   .^7C EB          jl      short CRACKME.00401D87      ; esi = 0123 となる
00401D9C   . 8A48 07        mov     cl, byte ptr ds:[eax+7]     ; 8 文字目
00401D9F   . 8A50 08        mov     dl, byte ptr ds:[eax+8]     ; 9 文字目
00401DA2   . C0E1 04        shl     cl, 4                       ; "CR0123-4567-89AB" なら
00401DA5   . 02CA           add     cl, dl                      ; cl = 45 となる
00401DA7   . 8A58 0A        mov     bl, byte ptr ds:[eax+A]     ; 11 文字目
00401DAA   . 8BD7           mov     edx, edi                    ; edx = 115599CC(固有ID)
00401DAC   . C1EA 18        shr     edx, 18                     ; edx = 00000011(固有IDの一部)
00401DAF   . 32CA           xor     cl, dl                      ; cl = 11 xor 45 が行われる ----+
00401DB1   . 8A50 09        mov     dl, byte ptr ds:[eax+9]     ; 10 文字目                     |
00401DB4   . C0E2 04        shl     dl, 4                       ; 10文字目 + 11文字目           |
00401DB7   . 02D3           add     dl, bl                      ; dl = 67                       |
00401DB9   . 8BDF           mov     ebx, edi                    ; ebx = 115599CC(固有ID)        |
00401DBB   . C1EB 10        shr     ebx, 10                     ; ebx = 00001155                |
00401DBE   . 32D3           xor     dl, bl                      ; dl = 67 xor 55(固有IDの一部)  |
00401DC0   . 8A58 0D        mov     bl, byte ptr ds:[eax+D]     ; パス 14 文字目                |
00401DC3   . 885424 10      mov     byte ptr ss:[esp+10], dl    ; 67 xor 55 の結果を格納        |
00401DC7   . 8A50 0C        mov     dl, byte ptr ds:[eax+C]     ; パス 13 文字目                |
00401DCA   . C0E2 04        shl     dl, 4                       ; bl = 9, dl = 80 になる        |
00401DCD   . 02D3           add     dl, bl                      ; "CR0123-4567-89AB"ならdl = 89 |
00401DCF   . 8B5C24 1C      mov     ebx, dword ptr ss:[esp+1C]  ; ebx = 115599CC(固有ID)        |
00401DD3   . C1EB 08        shr     ebx, 8                      ; ebx = 00115599                |
00401DD6   . 32D3           xor     dl, bl                      ; dl = 89 xor 99 -------------+ |
00401DD8   . 8A58 0E        mov     bl, byte ptr ds:[eax+E]     ; パス 15 文字目              | |
00401DDB   . C0E3 04        shl     bl, 4                       ; "CR0123-4567-89AB" なら     | |
00401DDE   . 0258 0F        add     bl, byte ptr ds:[eax+F]     ; パス 16 文字目, bl = AB     | |
00401DE1   . 8A4424 1C      mov     al, byte ptr ss:[esp+1C]    ; 固有ID の下2桁 "CC"        | |
00401DE5   . 884C24 14      mov     byte ptr ss:[esp+14], cl    ; cl  <-------------------------+
00401DE9   . 885424 0C      mov     byte ptr ss:[esp+C], dl     ; dl  <-----------------------+
00401DED   . 32D8           xor     bl, al                      ; 15,16文字目 xor CC(固有IDの一部) -+
00401DEF   . 3ACA           cmp     cl, dl                      ; 比較部分、等しいとダメ            |
00401DF1   . 885C24 1C      mov     byte ptr ss:[esp+1C], bl    ; <-格納----------------------------+
00401DF5   . 74 64          je      short CRACKME.00401E5B
00401DF7   . 3ACB           cmp     cl, bl                      ; 
00401DF9   . 74 60          je      short CRACKME.00401E5B
00401DFB   . 8B5424 0C      mov     edx, dword ptr ss:[esp+C]   ; 
00401DFF   . 8B4C24 1C      mov     ecx, dword ptr ss:[esp+1C]  ; ecx = 115599CC(固有ID)
00401E03   . 81E2 FF000000  and     edx, 0FF                    ; 余計な部分を排除
00401E09   . 81E1 FF000000  and     ecx, 0FF                    ; 同様に余計な部分を排除
00401E0F   . 8BC2           mov     eax, edx
00401E11   . 0FAFC2         imul    eax, edx
00401E14   . 0FAFC2         imul    eax, edx                    ; eax = edx * edx * edx
00401E17   . 8BD1           mov     edx, ecx
00401E19   . 0FAFD1         imul    edx, ecx
00401E1C   . 0FAFD1         imul    edx, ecx                    ; edx = ecx * ecx * ecx
00401E1F   . 03C2           add     eax, edx                    ; ここで3乗したもの同士足してる
00401E21   . 8B5424 14      mov     edx, dword ptr ss:[esp+14]  ; edx に 8,9 文字目 xor 固有ID 00xxxxxx が入る
00401E25   . 81E2 FF000000  and     edx, 0FF                    ; 
00401E2B   . 8B4C24 10      mov     ecx, dword ptr ss:[esp+10]  ; ecx に 10,11 文字目 xor 固有 ID xx00xxxx が入る
00401E2F   . 8BFA           mov     edi, edx                    ; edx に 8,9 文字目 xor 固有ID 00xxxxxx が入る
00401E31   . 81E1 FF000000  and     ecx, 0FF
00401E37   . 0FAFFA         imul    edi, edx
00401E3A   . 0FAFFA         imul    edi, edx                    ; edi = edx * edx * edx
00401E3D   . 8BD1           mov     edx, ecx
00401E3F   . 0FAFD1         imul    edx, ecx
00401E42   . 0FAFD1         imul    edx, ecx                    ; edx = ecx * ecx * ecx
00401E45   . 03FA           add     edi, edx                    ; 3乗したもの同士足してる
00401E47   . 3BF8           cmp     edi, eax                    ; 上の3乗、直前の3乗を比較
00401E49   . 75 10          jnz     short CRACKME.00401E5B      ; 等しいかな?
00401E4B   . 33C9           xor     ecx, ecx                    ; "CR0123-4567-89AB" なら
00401E4D   . 3BF0           cmp     esi, eax                    ; esi = 0123, これと eax を比較。
00401E4F   . 0F94C1         sete    cl                          ; 等しければ cl = 1 となる
00401E52   . 5F             pop     edi
00401E53   . 5E             pop     esi
00401E54   . 8BC1           mov     eax, ecx                    ; cl の値を eax (戻り値)に移している
00401E56   . 5B             pop     ebx
00401E57   . 83C4 0C        add     esp, 0C
00401E5A   . C3             retn
00401E5B   > 5F             pop     edi
00401E5C   . 5E             pop     esi
00401E5D   . 33C0           xor     eax, eax                    ; 戻り値 0
00401E5F   . 5B             pop     ebx
00401E60   . 83C4 0C        add     esp, 0C
00401E63   . C3             retn



これにより、トレースから得た情報をまとめてみると、

・正当パスの形式は CRxxxx-yyyy-zzzz で、xxxx, yyyy, zzzz は [0-9][A-F] の
範囲内となります([a-f] は範囲外)。

ID を 115599CC, パスを CR0123-4567-89AB とすると、

               45h  xor   11h              = 54h (ア) <--+--+ (00401D9A - 00401DAF)
( 8, 9 文字目結合)       (固有ID XXxxxxxx)               |  |
               67h  xor   55h              = 32h (イ)    ≠ | (00401DA7 - 00401DBE)
(10,11 文字目結合)       (固有ID xxXXxxxx)               |  ≠
               89h  xor   99h              = 10h (ウ) <--+  | (00401DC0 - 00401DD6)
(13,14 文字目結合)       (固有ID xxxxXXxx)                  |
               ABh  xor   CCh              = 67h (エ) <-----+ (00401DD8 - 00401DED)
(15,16 文字目結合)       (固有ID xxxxxxXX)

上記のような処理を行います。
さらに各値には次のような条件が定められています。

xxxx ≧ FFFFh(65535)                     ..(1) (00401D94, 4 桁しかないため最大値は FFFFh となる)
(ア) ≠ (ウ) かつ (ア) ≠ (エ)           ..(2) (00401DF1 - 00401DF9)
(ア)^3+(イ)^3 = (ウ)^3+(エ)^3 ..(3) (00401DFB - 00401E1F, 00401E21 - 00401E49)
(ウ)^3+(エ)^3 = xxxx               ..(4) (00401E4D)
条件(3)(4)より
(ア)^3+(イ)^3 = xxxx               ..(4') 

これらの条件を満たす (ア)(イ)(ウ)(エ) の値を導けばよいことがわかります。
ここで条件(1)(4)(4')について、 40^3 = FA00h(64000), 41^3 = 10D39h(68921) であることを考えると、
(ア) (イ) (ウ) (エ) は 40 以下の値でなければならないことがわかります。

a^3+b^3 ≦ FFFFh (a≦40 、b≦40)を満たす a、b を総当たりで求めると、

06c1 =  01^3 + 0c^3  =  09^3 + 0a^3
1008 =  02^3 + 10^3  =  09^3 + 0f^3
3608 =  02^3 + 18^3  =  12^3 + 14^3
50cb =  0a^3 + 1b^3  =  13^3 + 18^3
8040 =  04^3 + 20^3  =  12^3 + 1e^3
9990 =  02^3 + 22^3  =  0f^3 + 21^3
9c61 =  09^3 + 22^3  =  10^3 + 21^3    (ここではアルファベットは小文字になっていますが、
b65b =  03^3 + 24^3  =  1b^3 + 1e^3    大文字に直さないと不正解になります。注意!)
fae8 =  11^3 + 27^3  =  1a^3 + 24^3

9通りの解があることがわかりました。
今回は単にパス出しを行いたいので、9通りの解の中のひとつ06c1 =  01^3 + 0c^3  =  09^3 + 0a^3 に
着目して正解パスを作り出しましょう。ここでは固有ID は 11-5599CC として考えます。

まず第一段階として、"CR06C1-yyyy-zzzz" 、
固有ID から値を拾って2桁ずつに区切ります。

115599CC  →  11 55 99 CC

続いて、解となる式から値を拾い、同様に2桁ずつに区切ります。

06c1 =  01^3 + 0c^3  =  09^3 + 0a^3  →  01 0C 09 0A
        ~~     ~~       ~~     ~~
そして、先ほど区切った値同士を xor して値を出します。実は実際のルーチンでは2桁ずつ
区切って個別にチェックしていますが、ここでは別に2桁ずつに区切らなくても OK です。

115599CC xor 010C090A = 105990C6

これより 105990C6 が出たので、出た値を4桁で区切り、1059-90C6 とします。
以上を合わせると、固有ID = 115599CC に対応した正解パスは "CR06C1-1059-90C6" となります。
これは一例なのでまだ正解パスのパターンはあります。
キージェネを作成したいのであれば、全てのパターンについて理解する必要があります。

以上にて NORMAL の解説は終了になります。